package com.hisense.ovcloud.apicontrol.service;

import cn.hutool.json.JSONUtil;
import com.hisense.ovcloud.apicontrol.client.AuthServiceClient;
import com.hisense.ovcloud.apicontrol.config.Constants;
import com.hisense.ovcloud.apicontrol.dto.req.CheckReplay6Req;
import com.hisense.ovcloud.apicontrol.dto.req.GetAppSecretReq;
import com.hisense.ovcloud.apicontrol.errors.CommonRes;
import com.hisense.ovcloud.apicontrol.errors.ErrorCodes;
import com.hisense.ovcloud.apicontrol.errors.HitvException;
import com.hisense.ovcloud.apicontrol.tool.AES256CbcUtils;
import com.hisense.ovcloud.apicontrol.tool.ControllerUtil;
import com.hisense.ovcloud.apicontrol.tool.LocalLFUCacheUtil;
import com.hisense.ovcloud.common3.redis.RedisDao;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

import static com.hisense.ovcloud.apicontrol.config.Constants.CACHE_EXPIRE_SECONDS;
import static com.hisense.ovcloud.apicontrol.config.Constants.REDIS_NULL_VALUE;

@Service
@Slf4j
public class ApiControlService extends BaseAGService{
    @Value("${verify.signature.salt:D9519A4B756946F081B7BB5B5E8D1197}")
    private String SALT;

    @Value("${privacy.encrpt.key:aaaabbbbccccddddeeeeffffgggghhhh}")
    private String encryptKey;

    @Value("${sys.param.control.version:5.0}")
    private String newestVersion;

    @Value("${checkReplayTimestampInMinute}")
    private int checkReplayTimestampInMinute;

    @Value("${enableCheckNonce:true}")
    private boolean enableCheckNonce;

    private final LocalLFUCacheUtil localLFUCacheUtil;

    private final AuthServiceClient authServiceClient;

    private final RedisDao redisDao;

    @Value("${openapi.sign.rsa.privatekey:/usr/local/localconf/rsa_private_key.key}")
    private String rsaPrivateKeyBytesFilePath;

    public ApiControlService(LocalLFUCacheUtil localLFUCacheUtil, AuthServiceClient authServiceClient, RedisDao redisDao) {
        this.localLFUCacheUtil = localLFUCacheUtil;
        this.authServiceClient = authServiceClient;
        this.redisDao = redisDao;
    }

    public Mono<CommonRes> verify(CheckReplay6Req req) {
        CommonRes commonRes = CommonRes.builder()
                .ex(new HitvException(ErrorCodes.SIGN_CHECK_FAIL.value(), ErrorCodes.SIGN_CHECK_FAIL.desc()))
                .resultCode(ErrorCodes.SIGN_CHECK_FAIL.value())
                .desc(ErrorCodes.SIGN_CHECK_FAIL.desc())
                .data(false).build();
        String sign = req.getSign();
        String signEncode = URLEncoder.encode(req.getSign(), StandardCharsets.UTF_8);
        if (!ControllerUtil.verifyRsaSignatureWithSHA256(req.getParamsStr(), SALT, sign, rsaPrivateKeyBytesFilePath, req.getSrcType())
                &&
                !ControllerUtil.verifyRsaSignatureWithSHA256(req.getParamsStr(), SALT, signEncode, rsaPrivateKeyBytesFilePath, req.getSrcType())) {
            log.error("verify signature fail for signature {} of parameters: {}", sign, req.getParamsStr());
            return Mono.just(commonRes);
        }

        return checkTimestamp(req)
                .zipWhen(res -> {
                    if (!res) {
                        commonRes.setResultCode(ErrorCodes.TIMESTAMP_CHECK_FAIL.value());
                        commonRes.setEx(new HitvException(ErrorCodes.TIMESTAMP_CHECK_FAIL.value(), ErrorCodes.TIMESTAMP_CHECK_FAIL.desc()));
                        commonRes.setDesc(ErrorCodes.TIMESTAMP_CHECK_FAIL.desc());
                        return Mono.just(commonRes);
                    } else {
                        return checkNonce(req)
                                .zipWhen(res1 -> {
                                    if (!res1) {
                                        commonRes.setResultCode(ErrorCodes.NONCE_CHECK_FAIL.value());
                                        commonRes.setEx(new HitvException(ErrorCodes.NONCE_CHECK_FAIL.value(), ErrorCodes.NONCE_CHECK_FAIL.desc()));
                                        commonRes.setDesc(ErrorCodes.NONCE_CHECK_FAIL.desc());
                                        return Mono.just(commonRes);
                                    } else {
                                        return Mono.just(CommonRes.builder().data(true).build());
                                    }
                                }, (x, y) -> y);
                    }
                }, (x, y) -> y)
                .onErrorResume(ex -> {
                    log.error("ApiControlService verify {} failed! res: {}", req.getSign(), ex.getMessage());
                    return Mono.just(CommonRes.builder().data(false).build());
                });
    }

    public Mono<CommonRes> verifyMono(Mono<CheckReplay6Req> r) {
        return r.flatMap(this::verify);
    }

    public Mono<CommonRes> fetchAppSecret(Mono<String> appId) {
        return appId.flatMap(r -> {
            CommonRes commonRes = CommonRes.builder()
                    .ex(new HitvException(ErrorCodes.NO_APP_SECRET_FOUND_FROM_AUTH_SERVICE.value(), ErrorCodes.NO_APP_SECRET_FOUND_FROM_AUTH_SERVICE.desc()))
                    .desc(ErrorCodes.NO_APP_SECRET_FOUND_FROM_AUTH_SERVICE.desc())
                    .resultCode(ErrorCodes.NO_APP_SECRET_FOUND_FROM_AUTH_SERVICE.value())
                    .data(REDIS_NULL_VALUE).build();
            if (StringUtils.isEmpty(r)) {
                return Mono.just(commonRes);
            } else {
                return Mono.just(Optional.ofNullable(localLFUCacheUtil.getFromCache(r)).orElse(REDIS_NULL_VALUE))
                        .zipWhen(cachedAppSecretInLocalCache -> {
                            if (!cachedAppSecretInLocalCache.equals(REDIS_NULL_VALUE)) {
                                return Mono.just(CommonRes.builder().data(cachedAppSecretInLocalCache).build());
                            } else {
                                return redisDao.get(Constants.CACHE_PREFIX + r)
                                        .switchIfEmpty(Mono.just(REDIS_NULL_VALUE))
                                        .zipWhen(secretInRedis -> {
                                            if (!secretInRedis.equals(REDIS_NULL_VALUE)) {
                                                localLFUCacheUtil.setToCache(r, secretInRedis);
                                                return Mono.just(CommonRes.builder().data(secretInRedis).build());
                                            } else {
                                                return authServiceClient.getAppSecret(GetAppSecretReq.builder().appId(r).build())
                                                        .zipWhen(getAppSecretResp -> {
                                                            if (getAppSecretResp.getResultCode() == Constants.RESULT_SUCCESS){
                                                                if(getAppSecretResp.getClientType().equals(Constants.APP_SECRET_OUTER_CLIENT_TYPE)){
                                                                    commonRes.setEx(new HitvException(ErrorCodes.APPID_IS_NOT_INTERNAL.value(), ErrorCodes.APPID_IS_NOT_INTERNAL.desc()));
                                                                    commonRes.setDesc(ErrorCodes.APPID_IS_NOT_INTERNAL.desc());
                                                                    commonRes.setResultCode(ErrorCodes.APPID_IS_NOT_INTERNAL.value());
                                                                    return Mono.just(commonRes);
                                                                }

                                                                if (StringUtils.isNotEmpty(getAppSecretResp.getAppSecret())) {
                                                                    localLFUCacheUtil.setToCache(r, getAppSecretResp.getAppSecret());
                                                                    return redisDao.set(Constants.CACHE_PREFIX + r, getAppSecretResp.getAppSecret(), CACHE_EXPIRE_SECONDS)
                                                                            .zipWhen(res -> Mono.just(CommonRes.builder().data(getAppSecretResp.getAppSecret()).build()), (x, y) -> y);
                                                                }
                                                            }else{
                                                                commonRes.setResultCode(getAppSecretResp.getResultCode());
                                                            }

                                                            log.error("fetch appSecret failed for {} from localLFUCacheUtil for appId: {}", JSONUtil.toJsonStr(commonRes), r);
                                                            return Mono.just(commonRes);
                                                        }, (x, y) -> y);
                                            }
                                        }, (x, y) -> y);
                            }
                        }, (x, y) -> y);
            }
        });
    }

    public Mono<CommonRes> verify6(CheckReplay6Req req) {
        log.debug("verify6 {}", req.getParamsStr());
        return fetchAppSecret(Mono.just(req.getAppId()))
                .flatMap(commonResOfAppSecret -> {
                    CommonRes commonRes = CommonRes.builder()
                            .ex(new HitvException(ErrorCodes.NO_APP_SECRET_FOUND.value(), ErrorCodes.NO_APP_SECRET_FOUND.desc()))
                            .resultCode(ErrorCodes.NO_APP_SECRET_FOUND.value())
                            .data(false).build();

                    if (StringUtils.isEmpty(commonResOfAppSecret.getData().toString())) {
                        return Mono.just(commonRes);
                    }

                    String signCalculated = DigestUtils.sha1Hex(commonResOfAppSecret.getData().toString() + req.getParamsStr() + SALT);

                    log.info("paramsStr: {}, signCalculated: {}, sign: {}", req.getParamsStr(), signCalculated, req.getSign());

                    if (!Optional.of(signCalculated).orElse("SIGNISNULL").equalsIgnoreCase(req.getSign())) {
                        log.error("verify signature fail for appId {}, sign in request is {} but which calculated by api-control was {}", req.getAppId(), req.getSign(), signCalculated);
                        commonRes.setEx(new HitvException(ErrorCodes.SIGN_CHECK_FAIL.value(), ErrorCodes.SIGN_CHECK_FAIL.desc()));
                        commonRes.setResultCode(ErrorCodes.SIGN_CHECK_FAIL.value());
                        commonRes.setDesc(ErrorCodes.SIGN_CHECK_FAIL.desc());
                        return Mono.just(commonRes);
                    }

                    if (StringUtils.isEmpty(req.getNonce()) || req.getTimestamp() <= 0L) {
                        commonRes.setResultCode(ErrorCodes.PARAM_ERROR_INVALID.value());
                        commonRes.setEx(new HitvException(ErrorCodes.PARAM_ERROR_INVALID.value(), ErrorCodes.PARAM_ERROR_INVALID.desc()));
                        commonRes.setDesc(ErrorCodes.PARAM_ERROR_INVALID.desc());
                        return Mono.just(commonRes);
                    }

                    return checkTimestamp(req)
                            .zipWhen(res -> {
                                if (!res) {
                                    commonRes.setResultCode(ErrorCodes.TIMESTAMP_CHECK_FAIL.value());
                                    commonRes.setEx(new HitvException(ErrorCodes.TIMESTAMP_CHECK_FAIL.value(), ErrorCodes.TIMESTAMP_CHECK_FAIL.desc()));
                                    commonRes.setDesc(ErrorCodes.TIMESTAMP_CHECK_FAIL.desc());
                                    return Mono.just(commonRes);
                                } else {
                                    return checkNonce(req)
                                            .zipWhen(res1 -> {
                                                if (!res1) {
                                                    commonRes.setResultCode(ErrorCodes.NONCE_CHECK_FAIL.value());
                                                    commonRes.setEx(new HitvException(ErrorCodes.NONCE_CHECK_FAIL.value(), ErrorCodes.NONCE_CHECK_FAIL.desc()));
                                                    commonRes.setDesc(ErrorCodes.NONCE_CHECK_FAIL.desc());
                                                    return Mono.just(commonRes);
                                                } else {
                                                    return Mono.just(CommonRes.builder().data(true).build());
                                                }
                                            }, (x, y) -> y);
                                }
                            }, (x, y) -> y)
                            .onErrorResume(ex -> {
                                log.error("ApiControlService verify6 {} failed! res: {}", req.getSign(), ex.getMessage());
                                return Mono.just(CommonRes.builder().data(false).build());
                            });

                });
    }


    // encrypt must have Constants.SecurityParamters.SECURITY_DATA_PREFIX prefix
    public Mono<String> encrypt(String toEncrypt) {
        return Mono.just(Optional.ofNullable(AES256CbcUtils.encrypt256CBCWithoutIv(encryptKey, toEncrypt)).orElse(""));
    }

    public Mono<String> decrypt(String toDecrypt) {
        return Mono.just(Optional.ofNullable(AES256CbcUtils.decrypt256CBCWithoutIv(encryptKey, toDecrypt)).orElse(""));
    }

    private Mono<Boolean> checkNonce(CheckReplay6Req req) {
        if(Double.parseDouble(Optional.ofNullable(req.getVersion()).orElse("0.0")) < Double.parseDouble(Optional.ofNullable(newestVersion).orElse("6.0"))){
            return Mono.just(true);
        }

        if(enableCheckNonce){
            return redisDao.get(Constants.CLIFE_API_CONTROL_NONCE + req.getNonce())
                    .zipWhen(secretInRedis -> {
                        if (!secretInRedis.equals(REDIS_NULL_VALUE)) {
                            log.error("nonce check fail:{}", req.getNonce());
                            return Mono.just(false);
                        } else {
                            return redisDao.set(Constants.CLIFE_API_CONTROL_NONCE + req.getNonce(), req.getNonce(), checkReplayTimestampInMinute * 60)
                                    .zipWhen(res -> Mono.just(true), (x, y) -> y);
                        }
                    }, (x, y) -> y);
        }else{
            return Mono.just(true);
        }
    }


    private Mono<Boolean> checkTimestamp(CheckReplay6Req req) {
        if(Double.parseDouble(Optional.ofNullable(req.getVersion()).orElse("0.0")) < Double.parseDouble(Optional.ofNullable(newestVersion).orElse("6.0"))){
            return Mono.just(true);
        }

        log.info("checkReplayTimestamp: {}, {}", checkReplayTimestampInMinute, req.getTimestamp() - System.currentTimeMillis());
        boolean result = Math.abs(req.getTimestamp() - System.currentTimeMillis()) <= checkReplayTimestampInMinute * 60 * 1000L;
        if (!result) {
            log.error("{} for appId {} and parameter {}", ErrorCodes.TIMESTAMP_CHECK_FAIL.desc(), req.getAppId(), req.getParamsStr());
        }
        return Mono.just(Math.abs(req.getTimestamp() - System.currentTimeMillis()) <= checkReplayTimestampInMinute * 60 * 1000L);
    }

    private Mono<Boolean> needCheckReplay(CheckReplay6Req req) {
        if (StringUtils.isEmpty(req.getVersion())) {
            log.error("version is not present for appId {} and parameter {}", req.getAppId(), req.getParamsStr());
            return Mono.just(false);
        }
        double ver;
        try {
            ver = Double.parseDouble(req.getVersion());
        } catch (Exception e) {
            log.error("version format error:{}", req.getVersion());
            throw new HitvException(ErrorCodes.PARAM_FORMAT_ERROR.value(), ErrorCodes.PARAM_FORMAT_ERROR.desc());
        }
        return Mono.just(ver >= Double.parseDouble(newestVersion));
    }
}
